Skip to main content

Chapter 7: URDF, SDF & Unity Visualization

"A robot is only as good as its model. Accurate representation is the foundation of simulation and control."

Table of Contents​

  1. Introduction to Robot Description
  2. URDF Deep Dive
  3. SDF Format
  4. Xacro for Modularity
  5. Unity for Robotics
  6. ROS-Unity Integration
  7. Visualization Tools
  8. Best Practices

Introduction to Robot Description​

Robot description formats define the physical and visual properties of robots for simulation and visualization.

Format Comparison​

FormatPurposeStrengthsWeaknesses
URDFROS robot descriptionROS integration, simpleLimited features, no sensors
SDFGazebo simulationFull simulation supportMore complex
XacroURDF templatingReusable, parametricProcessing step needed
Unity3D visualizationBeautiful graphicsHeavier, game engine

URDF Deep Dive​

Complete URDF Structure​

<?xml version="1.0"?>
<robot name="mobile_manipulator">

<!-- Materials -->
<material name="blue">
<color rgba="0 0 0.8 1"/>
</material>

<material name="black">
<color rgba="0 0 0 1"/>
</material>

<material name="white">
<color rgba="1 1 1 1"/>
</material>

<!-- Base Link -->
<link name="base_link">
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<box size="0.6 0.4 0.2"/>
</geometry>
<material name="blue"/>
</visual>

<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<box size="0.6 0.4 0.2"/>
</geometry>
</collision>

<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="15.0"/>
<inertia ixx="0.175" ixy="0" ixz="0"
iyy="0.495" iyz="0"
izz="0.62"/>
</inertial>
</link>

<!-- Left Wheel -->
<link name="left_wheel">
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder length="0.05" radius="0.1"/>
</geometry>
<material name="black"/>
</visual>

<collision>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder length="0.05" radius="0.1"/>
</geometry>
</collision>

<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="1.0"/>
<inertia ixx="0.00277" ixy="0" ixz="0"
iyy="0.00277" iyz="0"
izz="0.005"/>
</inertial>
</link>

<!-- Left Wheel Joint -->
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="0.2 0.25 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
<dynamics damping="0.1" friction="0.1"/>
</joint>

<!-- Right Wheel -->
<link name="right_wheel">
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder length="0.05" radius="0.1"/>
</geometry>
<material name="black"/>
</visual>

<collision>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder length="0.05" radius="0.1"/>
</geometry>
</collision>

<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="1.0"/>
<inertia ixx="0.00277" ixy="0" ixz="0"
iyy="0.00277" iyz="0"
izz="0.005"/>
</inertial>
</link>

<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel"/>
<origin xyz="0.2 -0.25 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
<dynamics damping="0.1" friction="0.1"/>
</joint>

<!-- Caster Wheel -->
<link name="caster">
<visual>
<geometry>
<sphere radius="0.05"/>
</geometry>
<material name="white"/>
</visual>

<collision>
<geometry>
<sphere radius="0.05"/>
</geometry>
</collision>

<inertial>
<mass value="0.5"/>
<inertia ixx="0.0005" ixy="0" ixz="0"
iyy="0.0005" iyz="0"
izz="0.0005"/>
</inertial>
</link>

<joint name="caster_joint" type="fixed">
<parent link="base_link"/>
<child link="caster"/>
<origin xyz="-0.2 0 -0.05"/>
</joint>

<!-- Arm Links -->
<link name="arm_base">
<visual>
<geometry>
<cylinder length="0.1" radius="0.05"/>
</geometry>
<material name="blue"/>
</visual>

<collision>
<geometry>
<cylinder length="0.1" radius="0.05"/>
</geometry>
</collision>

<inertial>
<mass value="0.5"/>
<inertia ixx="0.001" ixy="0" ixz="0"
iyy="0.001" iyz="0"
izz="0.001"/>
</inertial>
</link>

<joint name="arm_base_joint" type="revolute">
<parent link="base_link"/>
<child link="arm_base"/>
<origin xyz="0 0 0.15" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit lower="-3.14" upper="3.14" effort="10" velocity="1.0"/>
</joint>

<!-- Add more arm links... -->

</robot>

Joint Types​

TypeDOFDescriptionUse Case
fixed0No movementSensors, static attachments
revolute1Rotation with limitsRobot joints
continuous1Unlimited rotationWheels
prismatic1Linear motionLinear actuators
planar2Plane motionMobile platforms
floating6Free motionUnderwater robots

Inertia Calculations​

# Inertia calculator utilities
import math

def box_inertia(mass, x, y, z):
"""Calculate inertia for box"""
ixx = (mass / 12.0) * (y**2 + z**2)
iyy = (mass / 12.0) * (x**2 + z**2)
izz = (mass / 12.0) * (x**2 + y**2)
return {'ixx': ixx, 'iyy': iyy, 'izz': izz}

def cylinder_inertia(mass, radius, length):
"""Calculate inertia for cylinder"""
ixx = (mass / 12.0) * (3 * radius**2 + length**2)
iyy = ixx
izz = 0.5 * mass * radius**2
return {'ixx': ixx, 'iyy': iyy, 'izz': izz}

def sphere_inertia(mass, radius):
"""Calculate inertia for sphere"""
i = 0.4 * mass * radius**2
return {'ixx': i, 'iyy': i, 'izz': i}

# Example usage
box = box_inertia(15.0, 0.6, 0.4, 0.2)
print(f"Box inertia: ixx={box['ixx']:.3f}, iyy={box['iyy']:.3f}, izz={box['izz']:.3f}")

SDF Format​

SDF vs URDF​

<?xml version="1.0"?>
<sdf version="1.8">
<model name="mobile_robot">

<!-- Model pose -->
<pose>0 0 0.1 0 0 0</pose>

<!-- Links -->
<link name="base_link">
<pose>0 0 0 0 0 0</pose>

<inertial>
<mass>15.0</mass>
<inertia>
<ixx>0.175</ixx>
<ixy>0</ixy>
<ixz>0</ixz>
<iyy>0.495</iyy>
<iyz>0</iyz>
<izz>0.62</izz>
</inertia>
</inertial>

<visual name="base_visual">
<geometry>
<box>
<size>0.6 0.4 0.2</size>
</box>
</geometry>
<material>
<ambient>0 0 0.8 1</ambient>
<diffuse>0 0 0.8 1</diffuse>
<specular>0.1 0.1 0.1 1</specular>
</material>
</visual>

<collision name="base_collision">
<geometry>
<box>
<size>0.6 0.4 0.2</size>
</box>
</geometry>
<surface>
<friction>
<ode>
<mu>0.6</mu>
<mu2>0.6</mu2>
</ode>
</friction>
</surface>
</collision>

<!-- Sensors in SDF -->
<sensor name="imu_sensor" type="imu">
<always_on>true</always_on>
<update_rate>100</update_rate>
<pose>0 0 0 0 0 0</pose>
</sensor>

<sensor name="camera" type="camera">
<pose>0.3 0 0.2 0 0 0</pose>
<camera>
<horizontal_fov>1.047</horizontal_fov>
<image>
<width>1920</width>
<height>1080</height>
</image>
<clip>
<near>0.1</near>
<far>100</far>
</clip>
</camera>
<always_on>true</always_on>
<update_rate>30</update_rate>
</sensor>
</link>

<!-- Joints -->
<joint name="left_wheel_joint" type="revolute">
<parent>base_link</parent>
<child>left_wheel</child>
<pose>0 0 0 0 0 0</pose>
<axis>
<xyz>0 1 0</xyz>
<dynamics>
<damping>0.1</damping>
<friction>0.1</friction>
</dynamics>
</axis>
</joint>

<!-- Plugins -->
<plugin name="differential_drive_controller"
filename="libgazebo_ros_diff_drive.so">
<update_rate>50</update_rate>
<left_joint>left_wheel_joint</left_joint>
<right_joint>right_wheel_joint</right_joint>
<wheel_separation>0.5</wheel_separation>
<wheel_diameter>0.2</wheel_diameter>
<max_wheel_torque>20</max_wheel_torque>
<command_topic>cmd_vel</command_topic>
<odometry_topic>odom</odometry_topic>
</plugin>

</model>
</sdf>

Xacro for Modularity​

Xacro Properties​

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="modular_robot">

<!-- Properties -->
<xacro:property name="wheel_radius" value="0.1"/>
<xacro:property name="wheel_width" value="0.05"/>
<xacro:property name="wheel_mass" value="1.0"/>
<xacro:property name="wheel_separation" value="0.5"/>

<xacro:property name="base_length" value="0.6"/>
<xacro:property name="base_width" value="0.4"/>
<xacro:property name="base_height" value="0.2"/>
<xacro:property name="base_mass" value="15.0"/>

<!-- PI constant -->
<xacro:property name="pi" value="3.14159265359"/>

<!-- Calculated properties -->
<xacro:property name="wheel_offset_y" value="${wheel_separation/2}"/>

</robot>

Xacro Macros​

<!-- Wheel macro -->
<xacro:macro name="wheel" params="prefix reflect">

<link name="${prefix}_wheel">
<visual>
<origin xyz="0 0 0" rpy="${pi/2} 0 0"/>
<geometry>
<cylinder length="${wheel_width}" radius="${wheel_radius}"/>
</geometry>
<material name="black"/>
</visual>

<collision>
<origin xyz="0 0 0" rpy="${pi/2} 0 0"/>
<geometry>
<cylinder length="${wheel_width}" radius="${wheel_radius}"/>
</geometry>
</collision>

<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="${wheel_mass}"/>
<xacro:cylinder_inertia m="${wheel_mass}"
r="${wheel_radius}"
h="${wheel_width}"/>
</inertial>
</link>

<joint name="${prefix}_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="${prefix}_wheel"/>
<origin xyz="0.2 ${reflect * wheel_offset_y} 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
<dynamics damping="0.1" friction="0.1"/>
</joint>

<gazebo reference="${prefix}_wheel">
<mu1>1.0</mu1>
<mu2>1.0</mu2>
<kp>10000000.0</kp>
<kd>1.0</kd>
<material>Gazebo/Black</material>
</gazebo>

</xacro:macro>

<!-- Inertia macros -->
<xacro:macro name="cylinder_inertia" params="m r h">
<inertia ixx="${m*(3*r*r+h*h)/12}" ixy="0" ixz="0"
iyy="${m*(3*r*r+h*h)/12}" iyz="0"
izz="${m*r*r/2}"/>
</xacro:macro>

<xacro:macro name="box_inertia" params="m x y z">
<inertia ixx="${m*(y*y+z*z)/12}" ixy="0" ixz="0"
iyy="${m*(x*x+z*z)/12}" iyz="0"
izz="${m*(x*x+y*y)/12}"/>
</xacro:macro>

<xacro:macro name="sphere_inertia" params="m r">
<inertia ixx="${2*m*r*r/5}" ixy="0" ixz="0"
iyy="${2*m*r*r/5}" iyz="0"
izz="${2*m*r*r/5}"/>
</xacro:macro>

<!-- Use macros -->
<xacro:wheel prefix="left" reflect="1"/>
<xacro:wheel prefix="right" reflect="-1"/>

Including External Files​

<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="complete_robot">

<!-- Include common properties -->
<xacro:include filename="$(find my_robot_description)/urdf/common_properties.xacro"/>

<!-- Include wheel macro -->
<xacro:include filename="$(find my_robot_description)/urdf/wheel.xacro"/>

<!-- Include sensor macros -->
<xacro:include filename="$(find my_robot_description)/urdf/sensors.xacro"/>

<!-- Use included macros -->
<xacro:wheel prefix="left" reflect="1"/>
<xacro:wheel prefix="right" reflect="-1"/>

<xacro:lidar_sensor parent="base_link" xyz="0.3 0 0.2"/>
<xacro:camera_sensor parent="base_link" xyz="0.3 0 0.25"/>

</robot>

Processing Xacro Files​

# Convert xacro to URDF
ros2 run xacro xacro robot.urdf.xacro > robot.urdf

# With parameters
ros2 run xacro xacro robot.urdf.xacro \
wheel_radius:=0.15 \
wheel_separation:=0.6 \
> robot_custom.urdf

# Check URDF validity
check_urdf robot.urdf

# Visualize URDF tree
urdf_to_graphiz robot.urdf

Unity for Robotics​

Unity Robotics Hub​

Unity provides photorealistic visualization and simulation capabilities.

Installation:

# Install Unity Hub
# Download from unity.com

# Install Unity Robotics packages via Package Manager:
# - ROS TCP Connector
# - URDF Importer
# - Robotics Visualizations

URDF Import to Unity​

Unity URDF Importer Settings:

// RobotImporter.cs
using UnityEngine;
using Unity.Robotics.UrdfImporter;

public class RobotImporter : MonoBehaviour
{
public string urdfPath = "Assets/URDF/robot.urdf";

void Start()
{
// Import URDF
GameObject robot = UrdfRobotExtensions.CreateRuntime(urdfPath);

// Configure physics
ConfigurePhysics(robot);

// Add visualization components
AddVisualization(robot);
}

void ConfigurePhysics(GameObject robot)
{
// Set up articulation bodies
foreach (var joint in robot.GetComponentsInChildren<ArticulationBody>())
{
joint.solverIterations = 20;
joint.solverVelocityIterations = 20;
}
}

void AddVisualization(GameObject robot)
{
// Add custom shaders, effects
var renderer = robot.GetComponentInChildren<Renderer>();
if (renderer != null)
{
renderer.material.shader = Shader.Find("Standard");
}
}
}

Unity Graphics Settings​

{
"render_pipeline": "URP",
"quality": {
"anti_aliasing": "4x MSAA",
"shadows": "Hard Shadows",
"texture_quality": "Full Res",
"vsync": true,
"target_fps": 60
},
"post_processing": {
"ambient_occlusion": true,
"bloom": true,
"color_grading": true,
"depth_of_field": false
}
}

ROS-Unity Integration​

ROS TCP Endpoint​

Unity → ROS 2 Communication:

using UnityEngine;
using Unity.Robotics.ROSTCPConnector;
using RosMessageTypes.Geometry;

public class RobotController : MonoBehaviour
{
private ROSConnection ros;
private string topicName = "/cmd_vel";

void Start()
{
ros = ROSConnection.GetOrCreateInstance();
ros.RegisterPublisher<TwistMsg>(topicName);
}

void Update()
{
// Get input
float linear = Input.GetAxis("Vertical");
float angular = Input.GetAxis("Horizontal");

// Create message
TwistMsg twist = new TwistMsg
{
linear = new Vector3Msg { x = linear, y = 0, z = 0 },
angular = new Vector3Msg { x = 0, y = 0, z = angular }
};

// Publish
ros.Publish(topicName, twist);
}
}

ROS 2 → Unity (Subscriber):

using Unity.Robotics.ROSTCPConnector;
using RosMessageTypes.Sensor;

public class SensorVisualizer : MonoBehaviour
{
private ROSConnection ros;

void Start()
{
ros = ROSConnection.GetOrCreateInstance();
ros.Subscribe<LaserScanMsg>("/scan", OnScanReceived);
}

void OnScanReceived(LaserScanMsg scan)
{
// Visualize LiDAR data
for (int i = 0; i < scan.ranges.Length; i++)
{
float angle = scan.angle_min + i * scan.angle_increment;
float range = scan.ranges[i];

Vector3 point = new Vector3(
range * Mathf.Cos(angle),
0,
range * Mathf.Sin(angle)
);

Debug.DrawLine(Vector3.zero, point, Color.red, 0.1f);
}
}
}

Launch ROS TCP Endpoint​

# ros_tcp_endpoint.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
return LaunchDescription([
Node(
package='ros_tcp_endpoint',
executable='default_server_endpoint',
parameters=[{
'ROS_IP': '192.168.1.100',
'ROS_TCP_PORT': 10000
}],
output='screen'
)
])

Visualization Tools​

RViz2​

# Launch RViz2
rviz2

# With config file
rviz2 -d robot.rviz

# From launch file
Node(
package='rviz2',
executable='rviz2',
arguments=['-d', rviz_config_path],
output='screen'
)

RViz Config Example:

Panels:
- Class: rviz_common/Displays
- Class: rviz_common/Views
- Class: rviz_common/Time

Visualization Manager:
Displays:
- Class: rviz_default_plugins/RobotModel
Robot Description: robot_description
Visual Enabled: true

- Class: rviz_default_plugins/TF
Frames:
All Enabled: true

- Class: rviz_default_plugins/LaserScan
Topic: /scan
Size: 0.05
Color: 255; 0; 0

- Class: rviz_default_plugins/Camera
Topic: /camera/image_raw

Joint State Publisher GUI​

# Install
sudo apt install ros-humble-joint-state-publisher-gui

# Launch
ros2 run joint_state_publisher_gui joint_state_publisher_gui

Launch with robot:

Node(
package='joint_state_publisher_gui',
executable='joint_state_publisher_gui',
output='screen'
),
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{'robot_description': robot_description}]
)

Best Practices​

URDF Best Practices​

✅ DO:

  • Use realistic mass and inertia values
  • Add collision meshes separate from visual
  • Use xacro for modularity
  • Define materials consistently
  • Include proper joint limits

❌ DON'T:

  • Use zero or very small masses
  • Ignore collision geometry
  • Hardcode values (use properties)
  • Forget to define axes
  • Use complex meshes for collision

File Organization​

robot_description/
├── urdf/
│ ├── robot.urdf.xacro # Main file
│ ├── common_properties.xacro # Shared properties
│ ├── wheel.xacro # Wheel macro
│ ├── sensors.xacro # Sensor macros
│ └── gazebo.xacro # Gazebo-specific
├── meshes/
│ ├── visual/ # High-poly meshes
│ │ ├── base_link.dae
│ │ └── arm_link.dae
│ └── collision/ # Low-poly meshes
│ ├── base_link.stl
│ └── arm_link.stl
├── config/
│ └── robot.rviz
└── launch/
└── display.launch.py

Performance Tips​

AspectRecommendation
Mesh complexityUse simplified collision meshes
Material countMinimize unique materials
Joint countOnly model necessary DOFs
Sensor rateMatch actual hardware rates
Update frequencyBalance accuracy vs performance

Summary​

Robot description formats are the foundation of simulation and visualization. Master URDF/SDF for accurate robot modeling, use xacro for maintainability, and leverage Unity for beautiful visualization.

Key Takeaways:

  1. ✅ URDF for ROS integration
  2. ✅ SDF for Gazebo simulation
  3. ✅ Xacro for modular, reusable descriptions
  4. ✅ Unity for photorealistic visualization
  5. ✅ Proper inertia and physics properties are critical
  6. ✅ Organize models for maintainability

Next Chapter:

Chapter 8 will introduce NVIDIA Isaac Sim, a cutting-edge photorealistic simulation platform with GPU acceleration.


This chapter covered robot description formats and visualization. You can now create accurate robot models for simulation and beautiful visualizations for presentation.